import fs from 'fs'
import walk from 'walk-sync'
import path from 'path'
import { escapeRegExp } from 'lodash-es'
import { Tokenizer } from 'liquidjs'
import frontmatter from '@/frame/lib/read-frontmatter'
import { allVersions } from '@/versions/lib/all-versions'
import { deprecated, oldestSupported } from '@/versions/lib/enterprise-server-releases'

const allVersionKeys = Object.values(allVersions)
const dryRun = ['-d', '--dry-run'].includes(process.argv[2])

const walkFiles = (pathToWalk: string, ext: string): string[] => {
  return walk(path.posix.join(process.cwd(), pathToWalk), {
    includeBasePath: true,
    directories: false,
  }).filter((file) => file.endsWith(ext) && !file.endsWith('README.md'))
}

const markdownFiles = walkFiles('content', '.md').concat(walkFiles('data', '.md'))
const yamlFiles = walkFiles('data', '.yml')

interface ReplacementsMap {
  [key: string]: string
}

interface VersionData {
  versions?: Record<string, string> | string
  [key: string]: any
}

interface OperatorsMap {
  [key: string]: string
  '==': string
  ver_gt: string
  ver_lt: string
  '!=': string
}

const operatorsMap: OperatorsMap = {
  // old: new
  '==': '=',
  ver_gt: '>',
  ver_lt: '<',
  '!=': '!=', // noop
}

// [start-readme]
//
// Run this script to convert long form Liquid conditionals (e.g., {% if currentVersion == "free-pro-team" %}) to
// the new custom tag (e.g., {% ifversion fpt %}) and also use the short names in versions frontmatter.
//
// [end-readme]

async function main() {
  if (dryRun)
    console.log('This is a dry run! The script will not write any files. Use for debugging.\n')

  // 1. UPDATE MARKDOWN FILES (CONTENT AND REUSABLES)
  console.log('Updating Liquid conditionals and versions frontmatter in Markdown files...\n')
  for (const file of markdownFiles) {
    // A. UPDATE LIQUID CONDITIONALS IN CONTENT
    // Create an { old: new } conditionals object so we can get the replacements and
    // make the replacements separately and not do both in nested loops.
    const content = fs.readFileSync(file, 'utf8')
    const contentReplacements = getLiquidReplacements(content, file)
    const newContent = makeLiquidReplacements(contentReplacements, content)

    // B. UPDATE FRONTMATTER VERSIONS PROPERTY
    const { data } = frontmatter(newContent) as { data: VersionData }
    if (data.versions && typeof data.versions !== 'string') {
      const versions = data.versions as Record<string, string>
      for (const [plan, value] of Object.entries(versions)) {
        // Update legacy versioning while we're here
        const valueToUse = value
          .replace('2.23', '3.0')
          .replace(`>=${oldestSupported}`, '*')
          .replace(/>=?2\.20/, '*')
          .replace(/>=?2\.19/, '*')

        // Find the relevant version from the master list so we can access the short name.
        const versionObj = allVersionKeys.find(
          (version) => version.plan === plan || version.shortName === plan,
        )
        if (!versionObj) {
          console.error(`can't find supported version for ${plan}`)
          process.exit(1)
        }
        delete versions[plan]
        versions[versionObj.shortName] = valueToUse
      }
    }

    if (dryRun) {
      console.log(contentReplacements)
    } else {
      // Using any for frontmatter.stringify options as gray-matter types don't include lineWidth
      fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 } as any))
    }
  }

  // 2. UPDATE LIQUID CONDITIONALS IN DATA YAML FILES
  console.log('Updating Liquid conditionals in YAML files...\n')
  for (const file of yamlFiles) {
    const yamlContent = fs.readFileSync(file, 'utf8')
    const yamlReplacements = getLiquidReplacements(yamlContent, file)
    // Update any `versions` properties in the YAML as well (learning tracks, etc.)
    const newYamlContent = makeLiquidReplacements(yamlReplacements, yamlContent)
      .replace(/("|')?free-pro-team("|')?:/g, 'fpt:')
      .replace(/("|')?enterprise-server("|')?:/g, 'ghes:')

    if (dryRun) {
      console.log(yamlReplacements)
    } else {
      fs.writeFileSync(file, newYamlContent)
    }
  }
}

try {
  await main()
  console.log('Done!')
} catch (err) {
  console.error(err)
  process.exit(1)
}

// Convenience function to help with readability by removing this large but unneded property.
// Using any for token objects as liquidjs doesn't provide TypeScript types
function removeInputProps(arrayOfObjects: any[]): any[] {
  return arrayOfObjects.map((obj: any) => {
    delete obj.input
    delete obj.token.input
    return obj
  })
}

function makeLiquidReplacements(replacementsObj: ReplacementsMap, text: string): string {
  let newText = text
  for (const [oldCond, newCond] of Object.entries(replacementsObj)) {
    const oldCondRegex = new RegExp(`({%-?)\\s*?${escapeRegExp(oldCond)}\\s*?(-?%})`, 'g')
    newText = newText
      .replace(oldCondRegex, `$1 ${newCond} $2`)
      // Content files use an old-school hack to ensure our old regex deprecation script DTRT, for example:
      // `if enterpriseServerVersions contains currentVersion and currentVersion ver_gt "enterprise-server@2.21"`
      // This script will change the above to `if ghes and ghes > 2.21`.
      // But we don't need the hack for the new deprecation script, because it will change `if ghes > 2.21` to `if ghes`.
      // So we can update this to the simpler `{% if ghes > 2.21 %}`.
      .replace(/ghes and ghes/g, 'ghes')
  }

  return newText
}

// Versions map:
// if currentVersion == "myVersion@myRelease" -> ifversion myVersionShort OR ifversion myVersionShort = @myRelease
// if currentVersion != "myVersion@myRelease" -> ifversion not myVersionShort OR ifversion myVersionShort != @myRelease
// if currentVersion ver_gt "myVersion@myRelease -> ifversion myVersionShort > myRelease
// if currentVersion ver_lt "myVersion@myRelease -> ifversion myVersionShort < myRelease
// if enterpriseServerVersions contains currentVersion -> ifversion ghes
function getLiquidReplacements(content: string, file: string): ReplacementsMap {
  const replacements: ReplacementsMap = {}

  const tokenizer = new Tokenizer(content)
  const tokens = removeInputProps(tokenizer.readTopLevelTokens())

  tokens
    .filter(
      (token) =>
        (token.name === 'if' || token.name === 'elsif') && token.content.includes('currentVersion'),
    )
    .map((token) => token.content)

  const conditionalTokens = tokens
    .filter(
      (xtoken) =>
        (xtoken.name === 'if' || xtoken.name === 'elsif') &&
        xtoken.content.includes('currentVersion'),
    )
    .map((xtoken) => xtoken.content)
  for (const token of conditionalTokens) {
    const newToken = token.startsWith('if') ? ['ifversion'] : ['elsif']
    // Everything from here on pushes to the `newToken` array to construct the new conditional.
    for (const op of token.replace(/(if|elsif) /, '').split(/ (or|and) /)) {
      if (op === 'or' || op === 'and') {
        newToken.push(op)
        continue
      }

      // This string will always resolve to `ifversion ghes`.
      if (op.includes('enterpriseServerVersions contains currentVersion')) {
        newToken.push('ghes')
        continue
      }

      // For the rest, we need to check the release string.

      // E.g., [ 'currentVersion', '==', '"enterprise-server@3.0"'].
      const opParts = op.split(' ')

      if (!(opParts.length === 3 && opParts[0] === 'currentVersion')) {
        console.error(`Something went wrong with ${token} in ${file}`)
        process.exit(1)
      }

      const operator = opParts[1]
      // Remove quotes around the version and then split it on the at sign.
      const [plan, release] = opParts[2].slice(1, -1).split('@')

      // Find the relevant version from the master list so we can access the short name.
      const versionObj = allVersionKeys.find((version) => version.plan === plan)

      if (!versionObj) {
        console.error(`Couldn't find a version for ${plan} in "${token}" in ${file}`)
        process.exit(1)
      }

      // Handle numbered releases!
      if (versionObj.hasNumberedReleases) {
        const newOperator: string | undefined = operatorsMap[operator]
        if (!newOperator) {
          console.error(
            `Couldn't find an operator that corresponds to ${operator} in "${token} in "${file}`,
          )
          process.exit(1)
        }

        // Account for this one weird version included in a couple content files
        deprecated.push('1.19')

        // E.g., ghes > 2.20
        const availableInAllGhes = deprecated.includes(release) && newOperator === '>'

        // We can change > deprecated releases, like ghes > 2.19, to just ghes.
        // These are now available for all ghes releases.
        if (availableInAllGhes) {
          newToken.push(versionObj.shortName)
          continue
        }

        // E.g., ghes < 2.20
        const lessThanDeprecated = deprecated.includes(release) && newOperator === '<'
        // E.g., ghes < 2.21
        const lessThanOldestSupported = release === oldestSupported && newOperator === '<'
        // E.g., ghes = 2.20
        const equalsDeprecated = deprecated.includes(release) && newOperator === '='
        const hasDeprecatedContent =
          lessThanDeprecated || lessThanOldestSupported || equalsDeprecated

        // Remove these by hand.
        if (hasDeprecatedContent) {
          console.error(`Found content that needs to be removed! See "${token} in "${file}`)
          process.exit(1)
        }

        // Override for legacy 2.23, which should be 3.0
        const releaseToUse = release === '2.23' ? '3.0' : release

        newToken.push(`${versionObj.shortName} ${newOperator} ${releaseToUse}`)
        continue
      }

      // Turn != into nots, now that we can assume this is not a numbered release.
      if (operator === '!=') {
        newToken.push(`not ${versionObj.shortName}`)
        continue
      }

      // We should only have equality conditionals left.
      if (operator !== '==') {
        console.error(`Expected == but found ${operator} in "${op}" in ${token}`)
        process.exit(1)
      }

      // Handle `latest`!
      if (release === 'latest') {
        newToken.push(versionObj.shortName)
        continue
      }

      // Handle all other non-standard releases, like github-ae@next and github-ae@issue-12345
      newToken.push(`${versionObj.shortName}-${release}`)
    }

    replacements[token] = newToken.join(' ')
  }

  return replacements
}
